package com.fjsh.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fjsh.entity.ElasticsearchEntity;
import com.fjsh.entity.ElasticsearchPageEntity;
import com.fjsh.entity.ElasticsearchParam;
import org.elasticsearch.action.admin.indices.alias.Alias;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.*;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.GeoDistanceSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Component
public class ElasticSearchUtil {

    @Resource
    RestHighLevelClient restHighLevelClient;

    /**
     * Description:  创建索引
     *
     * @param index 注意：必须是小写
     * @return
     * @throws IOException
     */
    public boolean createIndex(@NotNull String index) throws Exception {
        CreateIndexRequest request = new CreateIndexRequest(index);
        request.settings(Settings.builder()
                .put("index.number_of_shards", 1)
                .put("index.number_of_replicas", 0)
        );
        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
        return createIndexResponse.isAcknowledged();
    }

    /**
     * Description:  创建索引
     *
     * @param index     注意：必须是小写
     * @param indexJson 通过引入json 文档创建
     * @return
     * @throws Exception
     */
    public boolean createIndex(@NotNull String index, String indexJson) throws Exception {
        CreateIndexRequest request = new CreateIndexRequest(index);
        // 分片和副本数量
        request.source(indexJson, XContentType.JSON);
        // 创建索引
        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
        return createIndexResponse.isAcknowledged();

    }

    /**
     * Description: 索引创建
     *
     * @param index          索引名称,注意：必须是小写
     * @param aliases        索引别名
     * @param shardNumber    分片数量
     * @param replicaNumber  副本数量
     * @param mappingBuilder mapping Json 字符串
     */
    public boolean createIndex(@NotNull String index, String aliases, Integer shardNumber, Integer replicaNumber, XContentBuilder mappingBuilder) throws Exception {
        CreateIndexRequest request = new CreateIndexRequest(index);
        // 分片和副本数量
        request.settings(Settings.builder()
                .put("index.number_of_shards", shardNumber)
                .put("index.number_of_replicas", replicaNumber)
        );

        request.alias(new Alias(aliases));
        //定义该索引结构
        if (mappingBuilder != null) {
            request.mapping(mappingBuilder);
        }
        // 创建索引
        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
        return createIndexResponse.isAcknowledged();
    }

    /**
     * Description: 判断某个index是否存在
     *
     * @param index index名
     * @return boolean
     */
    public boolean indexExist(@NotNull String index) throws Exception {
        GetIndexRequest request = new GetIndexRequest(index);
        request.local(false);
        request.humanReadable(true);
        request.includeDefaults(false);
        return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
    }

    /**
     * Description: 删除index
     *
     * @param index index
     * @return void
     */
    public boolean deleteIndex(@NotNull String index) {
        try {
            return restHighLevelClient.indices().delete(new DeleteIndexRequest(index), RequestOptions.DEFAULT).isAcknowledged();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * Description: 插入/更新一条记录
     *
     * @param index  index
     * @param entity 对象
     */
    public boolean insertOrUpdateOne(@NotNull String index, ElasticsearchEntity entity) {
        IndexRequest request = new IndexRequest(index);
        request.id(entity.getId());
        request.source(JSON.toJSONStringWithDateFormat(entity.getData(), "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteDateUseDateFormat), XContentType.JSON);
        try {
            IndexResponse indexResponse = restHighLevelClient.index(request, RequestOptions.DEFAULT);
            if (indexResponse.status() == RestStatus.ACCEPTED || indexResponse.status() == RestStatus.CREATED)
                return true;
            else
                return false;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Description: 批量插入数据
     *
     * @param index index
     * @param list  带插入列表
     */
    public BulkResponse insertBatch(@NotNull String index, List<ElasticsearchEntity> list) {
        BulkRequest request = new BulkRequest();
        list.forEach(item -> request.add(new IndexRequest(index).id(item.getId())
                .source(JSON.toJSONStringWithDateFormat(item.getData(), "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteDateUseDateFormat), XContentType.JSON)));
        try {
            return restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Description: 批量更新数据
     *
     * @param index index
     * @param list  带更新列表
     */
    public BulkResponse updateBatch(@NotNull String index, List<ElasticsearchEntity> list) {
        BulkRequest request = new BulkRequest();
        list.forEach(item -> request.add(new UpdateRequest(index, item.getId())
                .doc(JSON.toJSONStringWithDateFormat(item.getData(), "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteDateUseDateFormat), XContentType.JSON)));
        try {
            return restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Description: 批量删除
     *
     * @param index  index
     * @param idList 待删除列表
     */
    public <T> void deleteBatch(@NotNull String index, Collection<T> idList) {
        BulkRequest request = new BulkRequest();
        idList.forEach(item -> request.add(new DeleteRequest(index, item.toString())));
        try {
            restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * Description: 通过id获取详情
     *
     * @param index 索引
     * @param id    数据id
     * @return
     * @throws IOException
     */
    public <T> T getById(@NotNull String index, @NotNull String id, Class<T> c) throws IOException {
        GetRequest request = new GetRequest(index).id(id);
        GetResponse getResponse = restHighLevelClient.get(request, RequestOptions.DEFAULT);
        if (getResponse.getSource() != null) {
            return JSON.parseObject(getResponse.getSourceAsString(), c);
        } else
            return null;
    }

    /**
     * Description: 搜索，简单条件检索（不含聚合，不分页)
     *
     * @param index   index
     * @param builder 查询参数
     * @param c       结果类对象
     * @return java.util.ArrayList
     */
    public <T> List<T> search(@NotNull String index, SearchSourceBuilder builder, Class<T> c, @Nullable String highlightField) {
        SearchRequest searchRequest = new SearchRequest(index);
        // 高亮
        if (!StringUtils.isEmpty(highlightField)) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            // 设置高亮字段
            highlightBuilder.field(highlightField);
            builder.highlighter(highlightBuilder);
        }
        searchRequest.source(builder);
        try {
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            List<T> res = new ArrayList<>(hits.length);
            for (SearchHit hit : hits) {
                res.add(JSON.parseObject(hit.getSourceAsString(), c));
            }
            return res;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Description:简单条件检索（不含聚合，不分页)
     *
     * @param index       索引
     * @param elasticsearchParam 检索条件
     * @return
     * @throws IOException
     */
    public <T> List<T> search(@NotNull String index, @NotNull ElasticsearchParam elasticsearchParam, Class<T> c) throws IOException {
        SearchRequest searchRequest = createSearchRequest(index, elasticsearchParam);
        try {
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            List<T> res = new ArrayList<>(hits.length);
            for (SearchHit hit : hits) {
                res.add(JSON.parseObject(hit.getSourceAsString(), c));
            }
            return res;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Description:分页条件检索(含聚合)
     *
     * @param index          索引
     * @param builder        查询参数
     * @param startPage      起始页码
     * @param pageSize       每页数量
     * @param c              结果对象类
     * @param highlightField 要高亮的属性（字段）命名
     * @return
     * @throws IOException
     */
    public <T> ElasticsearchPageEntity searchByPage(@NotNull String index, SearchSourceBuilder builder, int startPage, int pageSize, Class<T> c, String highlightField) throws IOException {
        return searchByPage(index, builder, startPage, pageSize, c, highlightField, true);
    }

    /**
     * Description:分页条件检索(含聚合)
     *
     * @param index          索引
     * @param builder        查询参数
     * @param startPage      起始页码
     * @param pageSize       每页数量
     * @param c              结果对象类
     * @param highlightField 要高亮的属性（字段）命名
     * @param showHits       是否返回查询hits值（非 aggs值）
     * @return
     * @throws IOException
     */
    public <T> ElasticsearchPageEntity searchByPage(@NotNull String index, SearchSourceBuilder builder, int startPage, int pageSize, Class<T> c, String highlightField, boolean showHits) throws IOException {

        SearchRequest searchRequest = new SearchRequest(index);
        //分页
        if (startPage < 1) startPage = 1;
        if (pageSize < 1) pageSize = 10;

        builder.from((startPage - 1) * pageSize).size(pageSize);

        // 高亮
        if (!StringUtils.isEmpty(highlightField)) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            // 设置高亮字段
            highlightBuilder.field(highlightField);
            builder.highlighter(highlightBuilder);
        }
        builder.fetchSource(showHits);

        searchRequest.source(builder);
        try {

            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            long totalHits = 0;
            List<T> res = null;
            if (showHits) {
                SearchHit[] hits = response.getHits().getHits();
                totalHits = response.getHits().getTotalHits().value;
                long length = hits.length;
                res = new ArrayList<>(hits.length);
                for (SearchHit hit : hits) {
                    res.add(JSON.parseObject(hit.getSourceAsString(), c));
                }
            }
            Aggregations aggregations = null;
            if (response.getAggregations() != null)
                aggregations = response.getAggregations();
            return new ElasticsearchPageEntity(0, 10, (int) totalHits, res, aggregations);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Description:条件检索不分页(含聚合)
     *
     * @param index          索引
     * @param builder        查询参数
     * @param c              结果对象类
     * @param highlightField 要高亮的属性（字段）命名
     * @return
     * @throws IOException
     */
    public <T> ElasticsearchPageEntity searchAll(@NotNull String index, SearchSourceBuilder builder, Class<T> c, String highlightField) throws IOException {
        return searchByPage(index, builder, 1, Integer.MAX_VALUE, c, highlightField);
    }


    /**
     * Description: 按 query 条件删除
     *
     * @param index   index
     * @param builder builder
     */
    public void deleteByQuery(@NotNull String index, QueryBuilder builder) {
        DeleteByQueryRequest request = new DeleteByQueryRequest(index);
        request.setQuery(builder);
        //设置批量操作数量,最大为10000
        request.setBatchSize(10000);
        request.setConflicts("proceed");
        try {
            restHighLevelClient.deleteByQuery(request, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * Description: 针对geo_point类型的查找，查询符合多边形内的数据
     *
     * @param index  索引
     * @param field  索引字段
     * @param points 构成多边形的点
     */
    public <T> List<T> searchGeoPolygon(@NotNull String index, String field, List<GeoPoint> points, Class<T> c) throws Exception {
        if (points == null || points.size() <= 0) {
            throw new Exception("geo points 值异常");
        }
        SearchSourceBuilder srb = new SearchSourceBuilder();
        GeoPolygonQueryBuilder qb = QueryBuilders.geoPolygonQuery(field, points);
        srb.query(qb);
        return search(index, srb, c, "");
    }

    /**
     * 查询矩形范围内的数据
     *
     * @param index        index
     * @param field        索引字段
     * @param bottom_right 右上坐标
     * @param top_left     左下坐标
     */
    public <T> List<T> searchGeoBoundingBox(@NotNull String index, String field, GeoPoint bottom_right, GeoPoint top_left, Class<T> c) throws Exception {
        if (bottom_right == null || top_left == null) {
            throw new Exception("geo points 值异常");
        }
        SearchSourceBuilder srb = new SearchSourceBuilder();
        GeoBoundingBoxQueryBuilder qb = QueryBuilders.geoBoundingBoxQuery(field)
                .setCorners(bottom_right, top_left);
        srb.query(qb);
        return search(index, srb, c, "");
    }

    /**
     * 查询距离中心点指定的范围内的位置
     *
     * @param index    index
     * @param field    索引字段
     * @param distance 距离
     * @param point    中心点
     * @return
     * @throws Exception
     */
    public <T> List<T> searchGeoDistance(@NotNull String index, String field, String distance, GeoPoint point, Class<T> c) throws Exception {
        if (point == null) {
            throw new Exception("geo points 值异常");
        }
        SearchSourceBuilder srb = new SearchSourceBuilder();
        QueryBuilder qb = QueryBuilders.geoDistanceQuery(field)
                .point(point)
                .distance(distance, DistanceUnit.KILOMETERS);
        srb.query(qb);
        GeoDistanceSortBuilder sort = SortBuilders.geoDistanceSort(field, point)
                .order(SortOrder.ASC)
                .unit(DistanceUnit.KILOMETERS);
        srb.sort(sort);
        return search(index, srb, c, "");
    }

    /**
     * Description: 建议匹配查找，查询符合相关数据
     *
     * @param index
     * @param suggestField    要查的字段名
     * @param suggestValue    要查的值
     * @param suggestMaxCount 最大返回条数
     * @return
     * @throws Exception
     */
    public <T> List<T> searchSuggest(@NotNull String index, String suggestField, String suggestValue, int suggestMaxCount, Class<T> c) throws Exception {
        SearchRequest searchRequest = new SearchRequest(index);
        CompletionSuggestionBuilder suggestionBuilderDistrict = new CompletionSuggestionBuilder(suggestField).prefix(suggestValue).size(suggestMaxCount);
        SuggestBuilder suggestBuilder = new SuggestBuilder();
        suggestBuilder.addSuggestion("suggest_user", suggestionBuilderDistrict);//添加suggest
        SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.suggest(suggestBuilder);
        searchRequest.source(builder);
        try {
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            Suggest suggest = response.getSuggest();
            CompletionSuggestion termSuggestion = suggest.getSuggestion("suggest_user");
            List<T> res = new ArrayList<>();
            for (CompletionSuggestion.Entry entry : termSuggestion.getEntries()) {
                for (CompletionSuggestion.Entry.Option option : entry) {
                    res.add(JSON.parseObject(option.getHit().getSourceAsString(), c));
                }
            }
            return res;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private SearchRequest createSearchRequest(String indexName, ElasticsearchParam elasticsearchParam) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.size(elasticsearchParam.getPageSize());
        searchSourceBuilder.from((elasticsearchParam.getPageNo() - 1) * elasticsearchParam.getPageSize());
        //添加检索条件
        elasticsearchParam.getBoolQueryBuilder().ifPresent(v -> searchSourceBuilder.query(v));
        //添加排序条件
        elasticsearchParam.getSortBuilder().forEach(v -> searchSourceBuilder.sort(v));
        //添加分类统计
        elasticsearchParam.getAggregation().forEach(agg -> searchSourceBuilder.aggregation(agg));
        //自定义bool检索条件
        elasticsearchParam.getBoolQueryBuilders().ifPresent(v -> searchSourceBuilder.query(v));
        //自定义query检索条件
        elasticsearchParam.getQuery().ifPresent(squery -> searchSourceBuilder.query(squery));
        SearchRequest searchRequest = new SearchRequest();
        return searchRequest.indices(indexName).source(searchSourceBuilder);
    }

}

